Writing Tool Instructions
This guide covers how to write effective instructions for Skills, SubAgents, and Collapse attributes. Good instructions are the difference between an agent that fumbles through tasks and one that executes them precisely.
The Instruction DSL
HPD-Agent uses a simple DSL (Domain Specific Language) for tool instructions:
| Feature | Syntax | Example |
|---|---|---|
| Plain text | String literals | "Always verify before deleting" |
| Multi-line | Newlines or \n | "Step 1: ...\nStep 2: ..." |
| Metadata interpolation | {metadata.Property} | "Search using {metadata.ProviderName}" |
| Dynamic expressions | Method/property references | nameof(GetInstructions) or direct call |
Skill Instructions
Skills use the same dual-context architecture as collapsed tools:
| Parameter | Location | Lifetime | Use For |
|---|---|---|---|
functionResult | Conversation history | One-time on activation | Additional context (appended to auto-generated message) |
systemPrompt | System prompt | Every turn while active | Workflow rules, constraints |
Important: The system automatically generates a base activation message:
"{SkillName} skill activated. Available functions: {FunctionList}"Your
functionResultis appended to this auto-generated message. Don't duplicate the activation info—use it only for additional context like tips, environment state, or configuration details. Passnullif the auto-generated message is sufficient.
Basic Format
SkillFactory.Create(
name: "Research",
description: "Deep research on a topic", // BEFORE activation
functionResult: null, // Auto-generated: "Research skill activated. Available functions: WebSearch, DeepRead"
systemPrompt: @"
RESEARCH WORKFLOW:
1. Start with a broad web search
2. Identify key sources and themes
3. Deep-dive into the most relevant sources
4. Synthesize findings into a coherent summary
5. Cite all sources used", // Persistent
"SearchTools.WebSearch",
"SearchTools.DeepRead"
);Do's ✓
Use functionResult for additional context only:
// Good: Additional info beyond the auto-generated message
functionResult: "Connected to: production_db. Tip: Use Query('SHOW TABLES') first."
// Good: Auto-generated message is sufficient
functionResult: nullPut workflow rules in systemPrompt:
systemPrompt: @"
WORKFLOW:
1. Query the database schema first (use Query with 'DESCRIBE tables')
2. Identify relevant tables for the user's question
3. Write and execute the actual query
4. Format results as a markdown table
RULES:
- Always LIMIT results to 100 rows
- Use JOINs instead of multiple queries when possible"Include decision points in systemPrompt:
systemPrompt: @"
1. Search the web for initial context
2. IF the topic is code-related:
- Also search the codebase
- Check documentation
3. IF sources conflict:
- Prefer official documentation
- Note the discrepancy to the user"Specify output format:
systemPrompt: @"
After analysis, provide:
- Summary (2-3 sentences)
- Key findings (bullet points)
- Confidence level (High/Medium/Low)
- Sources used"Don'ts ✗
Redundant with auto-generated message (wastes tokens):
// Bad: Duplicates auto-generated "{SkillName} skill activated. Available functions: ..."
functionResult: "Database skill activated. Query, Insert, Update, Delete available."Too vague:
// Bad: No actionable guidance
functionResult: null
systemPrompt: null // No workflow!Too long:
// Bad: Wall of text the agent will ignore
systemPrompt: @"
[500 words of detailed instructions covering every edge case...]"Contradictory:
// Bad: Conflicting guidance
systemPrompt: @"
Always use the fastest approach.
Always be thorough and check everything."Toolkit Instructions: FunctionResult vs SystemPrompt
The [Toolkit] attribute has two instruction slots with different purposes:
[Toolkit(
"Database operations",
FunctionResult: "...", // One-time, appended to auto-generated message
SystemPrompt: "..." // Persistent, in system instructions
)]Important: Collapsed toolkits automatically generate a base expansion message:
"{ToolkitName} expanded. Available functions: {FunctionList}"Your
FunctionResultis appended to this auto-generated message. Don't duplicate the expansion info—use it only for additional context.
When to Use FunctionResult
Additional context beyond auto-generated message:
// Good: Runtime info the auto-message doesn't provide
FunctionResult: @"
Connected to: production database
Tables: users, orders, products, inventory
Tip: Use Query('SHOW TABLES') to see all tables"
// Good: Auto-generated message is sufficient
FunctionResult: nullWhen to Use SystemPrompt
Critical rules that must be enforced every turn:
SystemPrompt: @"
DATABASE SAFETY RULES (MUST FOLLOW):
- NEVER execute DELETE without a WHERE clause
- NEVER drop tables or modify schema
- Always use parameterized queries for user input
- LIMIT all SELECT queries to 1000 rows"Behavioral constraints:
SystemPrompt: @"
When using file operations:
- Always confirm before overwriting existing files
- Never access files outside the project directory
- Log all write operations"Do's ✓
Keep SystemPrompt short and critical:
// Good: Essential rules only
SystemPrompt: "Never DELETE without WHERE. Always use transactions for multi-step operations."Use FunctionResult for additional context:
// Good: Additional info beyond auto-generated message
FunctionResult: @"
Working directory: /home/user/project
Tip: Use ListDir before ReadFile to verify paths"Don'ts ✗
Don't duplicate the auto-generated message (wastes tokens):
// Bad: The system already generates this
FunctionResult: "FileTools expanded. Available functions: ReadFile, WriteFile, DeleteFile"Don't put ephemeral info in SystemPrompt:
// Bad: Status info doesn't belong in persistent instructions
SystemPrompt: "Connected to server at 192.168.1.1. Session started at 10:42 AM."Don't duplicate between both:
// Bad: Same content in both places wastes tokens
FunctionResult: "Always use transactions",
SystemPrompt: "Always use transactions"Don't put critical rules only in FunctionResult:
// Bad: Agent might forget this rule in later turns
FunctionResult: "CRITICAL: Never delete without confirmation"
// Should be in SystemPrompt insteadDynamic Instructions with Expressions
For runtime-generated instructions, use method or property references:
Static Method
[Toolkit(
"Search operations",
FunctionResult: nameof(GetSearchStatus)
)]
public class SearchToolkit
{
public static string GetSearchStatus()
{
var providers = EnabledProviders.Select(p => p.Name);
return $"Available providers: {string.Join(", ", providers)}";
}
}Instance Method
[Toolkit(
"Database operations",
FunctionResult: nameof(GetConnectionInfo)
)]
public class DatabaseToolkit
{
private readonly string _connectionString;
public string GetConnectionInfo()
{
return $"Connected to: {_connectionString}";
}
}Static Property
[Toolkit(
"File operations",
SystemPrompt: nameof(FileRules)
)]
public class FileToolkit
{
public static string FileRules => @"
FILE SAFETY:
- Confirm before overwrite
- No access outside project root";
}Do's ✓
Use expressions for dynamic content:
// Good: Runtime state in instructions
FunctionResult: nameof(GetAvailableModels)
public static string GetAvailableModels()
{
return $"Models loaded: {string.Join(", ", _loadedModels)}";
}Don'ts ✗
Don't use string literals where expressions are expected:
// Bad: Triggers HPDAG0103 warning
[Toolkit("...", FunctionResult: "literal string")]
// The generator expects a method reference, not a stringDon't use complex expressions:
// Bad: Operators not supported
FunctionResult: GetA() + GetB()
// Good: Wrap in a method
FunctionResult: nameof(GetCombinedInfo)
public static string GetCombinedInfo() => GetA() + GetB();SubAgent Instructions
SubAgents receive instructions via AgentConfig.SystemInstructions. These define the SubAgent's persona and behavior.
Basic Format
SubAgentFactory.Create(
name: "Code Reviewer",
description: "Reviews code for quality and best practices",
agentConfig: new AgentConfig
{
SystemInstructions = @"
You are an expert code reviewer specializing in C# and .NET.
When reviewing code:
1. Check for bugs and logic errors
2. Evaluate code style and readability
3. Identify security vulnerabilities
4. Suggest performance improvements
5. Rate overall quality (1-10)
Be constructive and specific. Provide code examples for suggestions."
}
);Do's ✓
Define a clear persona:
SystemInstructions: @"
You are a security auditor with expertise in OWASP Top 10 vulnerabilities.
Your role is to identify security issues, not general code quality."Set boundaries:
SystemInstructions: @"
You are a documentation writer.
SCOPE:
- Write and update documentation
- Generate API docs from code
- Create examples and tutorials
OUT OF SCOPE:
- Do not modify source code
- Do not make architectural decisions"Specify output format:
SystemInstructions: @"
After each review, provide:
## Summary
[2-3 sentence overview]
## Issues Found
- [Issue 1]: [Severity] - [Description]
## Recommendations
1. [Specific actionable recommendation]
## Rating: X/10"Don'ts ✗
Don't be vague about the role:
// Bad: What does "help" mean?
SystemInstructions: "Help the user with their code."Don't give conflicting goals:
// Bad: Speed vs thoroughness conflict
SystemInstructions: @"
Be extremely thorough and check everything.
Also, be as fast as possible and don't waste time."Metadata Interpolation
Use {metadata.PropertyName} in descriptions for runtime values:
public class SearchMetadata : IToolMetadata
{
public string ProviderName { get; set; } = "Default";
public int MaxResults { get; set; } = 10;
}
[AIFunction<SearchMetadata>]
[AIDescription("Search using {metadata.ProviderName}, returning up to {metadata.MaxResults} results")]
public async Task<string> Search(string query) { }Where It Works
[AIDescription]on functions[AIDescription]on parametersdescriptionin[Toolkit("...")]descriptioninSkillFactory.Create()descriptioninSubAgentFactory.Create()
Do's ✓
Use for user-facing configuration:
[AIDescription("Query the {metadata.DatabaseName} database")]Provide defaults in metadata class:
public string ProviderName { get; set; } = "Default"; // Never nullDon'ts ✗
Don't interpolate in instructions (only descriptions):
// Bad: Won't work in skill systemPrompt
systemPrompt: "Search using {metadata.ProviderName}"
// Good: Use dynamic expression instead
systemPrompt: nameof(GetInstructions)
public string GetInstructions() => $"Search using {_metadata.ProviderName}";Common Patterns
The Checklist Pattern
For methodical tasks:
systemPrompt: @"
PRE-FLIGHT CHECKLIST:
□ Verify input parameters are valid
□ Check user has required permissions
□ Confirm target doesn't already exist
EXECUTION:
□ Perform the operation
□ Verify success
□ Log the action
POST-FLIGHT:
□ Return confirmation to user
□ Clean up temporary resources"The Decision Tree Pattern
For conditional workflows:
systemPrompt: @"
1. Analyze the request type:
IF bug report:
→ Search for similar issues
→ Check recent commits
→ Identify potential causes
IF feature request:
→ Search existing features
→ Check roadmap/backlog
→ Assess feasibility
IF question:
→ Search documentation
→ Search codebase
→ Provide answer with sources"The Guardrails Pattern
For safety-critical operations:
SystemPrompt: @"
GUARDRAILS - NEVER VIOLATE:
✗ Never execute commands with 'rm -rf'
✗ Never access files outside /project
✗ Never expose credentials or secrets
✗ Never bypass permission checks
ALWAYS:
✓ Confirm destructive operations
✓ Validate user input
✓ Log sensitive operations"The Expert Persona Pattern
For SubAgents:
SystemInstructions: @"
You are Dr. Security, a cybersecurity expert with 20 years of experience.
EXPERTISE:
- OWASP Top 10 vulnerabilities
- Penetration testing methodology
- Secure coding practices
- Compliance (SOC2, GDPR, HIPAA)
COMMUNICATION STYLE:
- Direct and technical
- Always cite specific vulnerabilities (e.g., CWE-89)
- Provide severity ratings (Critical/High/Medium/Low)
- Include remediation steps
LIMITATIONS:
- You audit code, you don't write it
- You identify issues, you don't fix them
- Escalate findings over 'High' severity"Instruction Length Guidelines
| Context | Recommended Length | Why |
|---|---|---|
| FunctionResult | 1-3 lines | Orientation only, in history |
| SystemPrompt | 3-10 lines | Every turn, keep minimal but actionable |
| Skill functionResult | 1-2 lines | Tool availability notice |
| Skill systemPrompt | 5-15 lines | Workflow guidance |
| SubAgent SystemInstructions | 10-30 lines | Persona + scope + style |
Rule of thumb: If you can't read it in 10 seconds, it's too long.
Debugging Instructions
Check Generated Code
The source generator creates readable code. Find it in:
obj/Debug/net8.0/generated/HPD.Agent.SourceGenerator/Look for:
{ToolkitName}Registry.g.cs- Toolkit registration- Container function implementations
- Resolver methods for dynamic descriptions
Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Instructions not appearing | Wrong slot (FunctionResult vs SystemPrompt) | Check which slot matches your need |
| Instructions appearing every turn | Used SystemPrompt for ephemeral info | Move to FunctionResult |
| Dynamic instructions static | Using literal instead of expression | Use nameof(Method) |
| Compile error HPDAG0103 | String literal in expression slot | Use method reference |
Summary
| Instruction Type | Where | When Visible | Use For |
|---|---|---|---|
Skill functionResult | Appended to auto-generated activation message | Once on activation | Additional context (tips, state) |
Skill systemPrompt | System prompt | Every turn while active | Workflow steps |
Toolkit FunctionResult | Appended to auto-generated expansion message | Once on expansion | Additional context (tips, state) |
Toolkit SystemPrompt | System prompt | Every turn while expanded | Critical rules |
SubAgent SystemInstructions | System prompt | Always | Persona, scope |
{metadata.X} | Descriptions | Tool selection | Dynamic labels |
Golden rules:
- Be specific, not vague
- Be concise, not comprehensive
- Put critical rules in SystemPrompt (persistent)
- Put additional context in FunctionResult (ephemeral, appended to auto-generated message)
- Don't duplicate auto-generated messages—they waste tokens
- Test your instructions—if the agent ignores them, they're too long or unclear